# Logtrack-ng - Module for detecting login messages
# Copyright (C) 2007 Jan Kucera
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
# USA.

=head1 NAME

Logtrack::Syslog::Login - modul pro detekci a hlaseni neuspesnych prihlaseni

=head1 DESCRIPTION

Modul se stara o zapis a analyzu prihlaseni. Dane udalosti ukladam do incidentu
prostrednictvim modulu Logtrack::DBI. V pripade prekroceni 
nastavenych hodnot vola modul funkce mail_alert, ktera informuje spravce o 
neobvykle udalosti.

=head2 FUNCTIONS

=over 4

=item accept_login
=item check
=item mail_alert


=cut

package Logtrack::Syslog::Login;
use Logtrack::DBI;
use Logtrack::Debug;	
use Logtrack::Table;

use Exporter;
use Switch;
use Exception qw(:all);
use Logtrack::Mail;
use Data::Dumper;
use Net::DNS;


use constant TRUE => 1;
use constant FALSE => 0;

# use lib /root/perl;
# use RPCutils;
@ISA = qw(Exporter);
@EXPORT = qw(&failed_login &failed_su_login &accept_login &check);

use strict;
no strict 'refs';

our @EXPORT_OK;

use constant LOGIN_LIMIT    => 10;

=item
Constructor of login module, it collects all needed variables to process
expected functionality.
=cut

sub new {
	my ($self,$ref_logtrack,$id_event,$ref_message,$match) = @_;
	my $class = ref $self || $self;
	my $login = {
		debug => Logtrack::Debug->new()
	};
	bless ($login,$class);
	$login->{logtrack} = $ref_logtrack;
	$login->{db} = $$ref_logtrack->{dbi}->get_db();
	$login->{dbi} = $$ref_logtrack->{dbi};
	$login->{id_event} = $id_event;
	$login->{message} = $ref_message;
	$login->{params} = $match;
	$login->{table} = Logtrack::Table->new();
	return ($login);
}

sub errstr {
	my ($self,$message) = @_;
	$self->{debug}->errstr($message);
}

sub errstrln {
	my ($self,$message) = @_;
	$self->{debug}->errstrln($message);
}

sub debug {
	my ($self,$message,$level) = @_;
	$self->{debug}->debug($message,$level);
}

sub debugln {
	my ($self,$message,$level) = @_;
	$self->{debug}->debugln($message,$level);
}

=item failed_login
Function first creates object Login which takes care of incidents bussiness.
First object create incident(s) in DB and returns their IDs. Later those IDs
are checked whether records are considered as attacks or not. If it does, 
function returns array of related incidents IDs which will be processed by
mail_alert which creates text for report-mail. Then all related incidents are 
erased.
=cut

sub failed_login {
	my ($ref_logtrack,$id_event,$ref_message,@match) = @_;
	my @alert_id;
	my $login;
	my @dirty_incident;

	$login =  Logtrack::Syslog::Login->new(
		$ref_logtrack,$id_event,$ref_message,\@match
	);
	$login->debugln('Failed_login executed',2);
	
	try {
		@dirty_incident = $login->create_incident();
	}
	when ['DBI'], except {
		die 'Exception catched at creating incident'
			.'in Logtrack::Syslog::Login::failed_login';
	};
	
	while(my $id = pop @dirty_incident) {
		@alert_id = $login->check_incident($id);

		next if(@alert_id < 1);
		my $details = $login->find_details($id,@alert_id);
		$login->mailing_alert($id,$details,@alert_id);
		$login->blocking_sequence($details, $login->{table}->get_output() );
		$login->{dbi}->delete_incident($id,@alert_id);
	}
	
}

=item
Function only creates incident in DB and returns their IDs
=cut

sub create_incident {
	my ($self) = @_;
	my @dirty_incident;
	if(not defined $self->{params}[0] || not defined $self->{params}[1]) {
		$self->errstrln('Arguments not defined	in failed_login');
		return @dirty_incident;
	}

	$self->{params}[1] = get_address($self->{params}[1]);

	$self->debugln('Executing failed_login');
	push @dirty_incident, $self->{dbi}->insert_incident (
		$self->{id_event},'uzivatele',$self->{params}[0]
	);

	push @dirty_incident, $self->{dbi}->insert_incident (
		$self->{id_event},'cizi_stroje',$self->{params}[1]
	);
	
	return @dirty_incident;
}

=item
Function check, whether incidents doesn't exceed limits, if does function 
returns ID of related incidents
=cut
sub check_incident {
	my ($self,$id_incident) = @_;
	my $ref_db = $self->{db};
	my ($result,@alert);
	$result = $$ref_db->selectrow_hashref(
		'SELECT * FROM incidents WHERE id = ?',{},$id_incident
	);
		
	return if(not defined $result);
		
	if($result->{count} >= LOGIN_LIMIT && $result->{domain_key} =~ /root/) {
		return $self->get_related_incident($id_incident);
	}

	if($result->{count} >= LOGIN_LIMIT && $result->{domain} eq 'uzivatele') {
		return $self->get_related_incident($id_incident);
	}
	
	if($result->{count} >= LOGIN_LIMIT && $result->{domain} eq 'cizi_stroje') {
		return $self->get_related_incident($id_incident);
	}

	return ;
}

=item
Function returns IDs of related incidents with ID from argument $id_incident
=cut

sub get_related_incident {
	my ($self,$id_incident) = @_;
	my $ref_db = $self->{db};
	my @id;
	my $query = $$ref_db->prepare('
		SELECT id 
		FROM incidents WHERE id IN (
			SELECT DISTINCT id_incident 
			FROM events_incidents WHERE id_event IN (
				SELECT id_event 
				FROM events_incidents WHERE id_incident = ?
			)	AND id_incident != ?
		)'
	);
	$query->execute($id_incident,$id_incident);
	
	return undef if(not $query->rows);
	
	while(my $id = $query->fetchrow_array) {
		push @id, $id;
	}
	$query->finish;
	return @id;
}

sub find_details {
	my ($self,$id,@id_incidents) = @_;
	my($src,$dst,$description,$reason,$user,$count);
	my $ref_db = $self->{db};
	my ($row,@details);
	$dst = $self->{message}->{Machine}; #Attacked server address
	use Term::ANSIColor qw(:constants);
	my $query = $$ref_db->prepare(
		'SELECT domain,domain_key AS key,count 
		FROM incidents WHERE id = ?'
	);
	$query->execute($id);
	my ($alert_type,$key,undef) = $query->fetchrow_array;
	
	my %item = (
		'src' => '', 'dst' => '', 'user' => '', 'description' => '',
		'count' => ''
	);
	
	if(@id_incidents < 2) {
		$query->execute($id_incidents[0]);
		$reason = 'SSH utok na '. $dst;
		
		if($alert_type eq 'uzivatele') {
			(undef,$src,$count)= $query->fetchrow_array;
			$user = $key;
			$self->errstrln(
				$src .' -> '.$dst .' x '.$user.' - '. $count ,3
			);
		}	
		else {
			(undef,$user,$count)= $query->fetchrow_array;
			$src = $key;
			$self->errstrln(
				$src .' -> '.$dst .' x '.$user.' - '. $count ,3
				
			);
		}
		$description = $src .' -> '.$dst;
		my %item = (
			'src' => $src, 'dst' => $dst, 'user' => $user,'count' => $count,
 			'description' => $description,'reason' => $reason
		);
		push @details, \%item;
		$query->finish;
	}
	else { #Cizi stroje
		if($alert_type eq 'uzivatele') { #multiple attack
			$user = $key;
			$reason = 'SSH utok na '. $dst;
			$description = '#'.@id_incidents .' -> '. $key;
			foreach my $id_incident (@id_incidents) {
				$query->execute($id_incident);
				(undef,$src,$count) = $query->fetchrow_array;
				$self->errstrln(
					$src .' -> '.$dst .' x '.$user.' - '. $count ,3
				);
				my %item = (
					'src' => $src, 'dst' => $dst, 'user' => $user,
					'description' => $description,'count' => $count,
					'reason' => $reason
				);
				push @details, \%item;
			}
			$query->finish;
			
		}
		else {
			print RED 'Scanning users',RESET ."\n";
			$src = $key;
			$description = $key .' -> #'. @id_incidents;
			$reason = 'Skanovani uzivatelu na '. $dst;
			foreach my $id_incident (@id_incidents) {
				$query->execute($id_incident);
				(undef,$user,$count) = $query->fetchrow_array;
				$self->errstrln(
					$src .' -> '.$dst .' x '.$user.' - '. $count ,3
				);
				my %item = (
					'src' => $src, 'dst' => $dst, 'user' => $user,
					'description' => $description,'count' => $count,
					'reason' => $reason
				);
				push @details, \%item;
			}
			$query->finish;
		}
	}
	
	return \@details;
}

=item
Function gets IDs of main incident and its "relatives" and generates all 
necessary texts and data for sending email.
=cut

sub mailing_alert {
	my ($self,$id,$details,@id_incidents) = @_;
	my $ref_db = $self->{db};		#Reference to active opened database
	my $index = @id_incidents - 1;			#Count of items in array minus one
	my ($query,$row);
	my ($alert_type,$key,$count);				#DB row which matches with $id
	my (@labels,@logs,$table);		#Variable for displaying logs
	my $dst = $self->{message}->{Machine};		
	
	#Variables for mail report
	my ($subject,$header,$type,$description,$log);
	
	
	$query = $$ref_db->prepare(
		'SELECT domain,domain_key AS key,count 
		FROM incidents WHERE id = ?'
	);
	$query->execute($id);
	($alert_type,$key,undef) = $query->fetchrow_array;
	
	$query->finish();

	$subject = 'Logtrack: '. $$details[0]{description};
	
	if($alert_type eq 'uzivatele') {
		$header = ' - '. LOGIN_LIMIT;
		$header .= ' neuspesnych prihlaseni na uzivatele ';
		$header .= $$details[0]{user} ."\n";
		$type = 'z adres';
		foreach my $item ( @{$details} ) {
			$description .= ' - '. $item->{src};
			$description .= ' ' x ( 16 - length($item->{src}) );
			$description .= $item->{count} .' x'."\n";
		}	
	}
	else {
		$header = ' - '. LOGIN_LIMIT .' neuspesnych prihlaseni z IP adresy ';
		$header .= $$details[0]{src} ."\n";
		$type = 'na uzivatele';
		
		foreach my $item (@{$details}) {
			$description .= ' - '. $item->{user};
			$description .= ' ' x (16 - length($item->{user}));
			$description .= $item->{count} .' x'."\n";
		}
	}

	$query = $$ref_db->prepare(
	'SELECT message,count AS num
		FROM events_incidents,events WHERE id_incident IN (?'.',?' x $index .')
			AND id_event = events.id AND id_event 
		IN (
			SELECT id_event FROM	events_incidents WHERE id_incident = ?
		)'
	);
	
	$query->execute(@id_incidents,$id);
	
	if($query->rows) {
		while (my @line = $query->fetchrow_array) {
			push @logs, (\@line);
		}
	}
	$query->finish();
	
	@labels = $query->{NAME};
	
	$log = $self->{table}->draw_table(@labels,\@logs);	
	
	Logtrack::Mail->new( $subject,$header,$type,$description,$log );
}


=item
Funkce nejprve zjisti ID incidentu s poctem prihlaseni na uzivatele a nasledne
vyhleda vsechny zaznamy s IP adresou, ktera se autentizovala. Oboji je pak 
predano funkce delete_incident pro mazani zaznamu pres zavislosti.
=cut

sub accept_login {
	my ($ref_logtrack,$id_event,$ref_message,@match) = @_;
	my ($query,$result,$id,@id_change,$ref_db,$domain);
	
	my $login =  Logtrack::Syslog::Login->new(
		$ref_logtrack,$id_event,$ref_message,\@match
	);
	$ref_db = $login->{db};
	$login->debugln('Executing accept_login',3);
		
	$match[1] = get_address($match[1]);
	
	$result = $$ref_db->selectrow_hashref(
		'SELECT SUM(count) as sum FROM reports WHERE src = ?',{},$match[1]
	);

	# IP tried to hack only this machine and this account
	if( (defined $$result{sum}) && ($$result{sum} >= 5)) {
		my ($subject,$header,$type,$description,
			$log,$table,@result,@label
		);
		$login->errstrln('Warning: possible break-in detected');
		$subject = 'Logtrack: Break-in to '. $ref_message->{Machine};
		$header = ' - uspesne prihlaseni ze stroje '. $match[1];
		$header .= ' , ktery  v posledni dobe uskutecnil ';
		$header	.= $result->{sum} .' incidentu' ."\n";

		$query = $$ref_db->prepare(
			'SELECT  src, dst, login, dates.date, count
				FROM reports, (
					SELECT id, MAX (date) as date
						FROM report_dates GROUP BY id
				) AS dates WHERE dates.id = reports.id AND src=?'
		);
		
		$query->execute($match[1]);
		
		if($query->rows) {
			while(my $row = $query->fetchrow_arrayref) {
				push @result, $row;
			}
			@label = $query->{NAME};
			$table = Logtrack::Table->new();
			$header .= $table->draw_table(@label,\@result);	
			$query->finish();
		}
		


		$type = 'na stroj';
		$description = ' - '. $ref_message->{Machine} ."\n";
		
		$query = $$ref_db->prepare(
			'SELECT message FROM events WHERE id = ?'
		);
		
		$query->execute($id_event);
		
		@result = ();
		if($query->rows) {
			while(my $row = $query->fetchrow_arrayref) {
				push @result, $row;
				push @result, $row;
			}
			@label = $query->{NAME};
			$log .= $table->draw_table(@label,\@result);	
			$query->finish();
		}
		$query->finish();
		mail($subject,$header,$type,$description,$log);
		$$ref_db->do('DELETE FROM reports WHERE src=?',{},$match[1]);
	}

	$query = $$ref_db->prepare('SELECT id FROM incidents 
				WHERE domain = ? AND domain_key = ?'
	);
			
	$query->execute('uzivatele',$match[0]);
	$result = $query->fetchrow_hashref;

	return if(not $result);
	$id = $result->{id};
	
	$query->execute('cizi_stroje',$match[1]);
	$result = $query->fetchrow_hashref;
	return if(not $result);
	push @id_change,$result->{id};
	$query->finish();
	
	$login->{dbi}->delete_incident($id,@id_change);
}

=item
Function for detecting failed su logins - first checks paramaters, increase 
record in table INCIDENTS and if atribute COUNT exceeds, it find related events
and prepare data for sending mail by function mail

Funkce pro detekci neuspesnych zmen uzivatele. Funkce zkontroluje zda jsou 
parametry platne, nasledne vytvori nebo aktualizuje zaznamy v tabulce INCIDENTS
a kontroluje jeho pocet. Pokud prekracuje, je zaslan varovny mail.
=cut

sub failed_su_login {
	my ($ref_logtrack,$id_event,$ref_message,@match) = @_;
	my ($user,$ref_db,$id_incident,$result);
	my $login =  Logtrack::Syslog::Login->new(
		$ref_logtrack,$id_event,$ref_message,\@match
	);
	my $dst = $ref_message->{Machine};
	$user = $match[2] if($match[2] ne '');
	$user = $match[3] if($match[3] ne '');
	if($user eq '') {
		$login->errstrln('Failed_su_login has undefined arguments!');
		return 1;
	}
	$ref_db = $login->{db};
	$id_incident = $login->{dbi}->insert_incident (
		$id_event,'su_login',$dst
	);
	
	$result = $$ref_db->selectrow_hashref('SELECT * FROM incidents
				WHERE id = ?',{},$id_incident);
				
	if($result->{count} >= 40) {
		my ($subject,$header,$type,$description,$log);
		my $query = $$ref_db->prepare(
			'SELECT message, count FROM events WHERE id IN
				(SELECT id_event FROM events_incidents WHERE id_incident = ?)'
		);
				
		$query->execute($result->{id});
		if($query->rows) {
			my (@result,@label);
			while(my $row = $query->fetchrow_arrayref) {
				push @result, $row;
				@label = $query->{NAME};
			}
			$query->finish();
			
			my $table = Logtrack::Table->new();
				$log .= $table->draw_table(@label,\@result);	
				
			$subject = 'Logtrack: Local login -> '. $dst;
			$header = ' - '. $result->{count} .
				' neuspesnych lokalnich prihlaseni' ."\n";
			$type = 'na tyto stroje';
			$description = ' - '. $dst ."\t". $result->{count} ."x\n";
			mail($subject,$header,$type,$description,$log);
			$login->{dbi}->delete_incident($result->{id});
		}
		else {
			return undef;
		}
	}
}

=item
Function get one argument (domain name) and transfors it to address
=cut

sub get_address {
	my ($hostname) = @_;
	return $hostname if($hostname =~ /^\s*((\d{1,3}\.){3}\d{1,3})\s*$/);
	
	my $resolver = Net::DNS::Resolver->new;
	my $result = $resolver->search($hostname);

	if( defined $result ) {
		foreach my $record ($result->answer) {
			if($record->type eq 'A') {
				$hostname = $record->address;
				last;
			}
		}
	}

	return $hostname;
}


sub blocking_sequence {
	my ($self,$ref_details,$log) = @_;
	my $ip_count = @{$ref_details};
	foreach my $item (@{$ref_details}) {
		if( $item->{count} >= (LOGIN_LIMIT/$ip_count) ) {
			$self->errstrln('Blocking IP '. $item->{src} .
				'!! Reason: '. $item->{reason},3
			); 
# 			block_ip($item->{src},$item->{reason},$log);
		}
	}

}

=item 
Function for blocking IP on firewall - RPC must be executed by eval because
any bug ends with die statement.

Funkce na blokovani IP na firewallu v ramci site FI MU za vyuziti RPC. Funkce
musi byt spoustena pres eval nebot jakakoli chyba konci zavolanim funkce die.
=cut

sub block_ip() {
	my ($ip,$reason,$log) = @_;
	eval {
	my $rpc = rpc_client('arkas');
	$rpc->call('Sit.fw_add',$ip,$reason,30,$log);
	}
}

1;

__END__